// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';

import 'package:build_config/build_config.dart';
import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';
import 'package:graphs/graphs.dart';
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';

import '../build_plan/package_graph.dart';
import '../build_plan/target_graph.dart';
import '../constants.dart';
import '../exceptions.dart';
import '../io/reader_writer.dart';
import '../logging/build_log.dart';

final _lastShortFormatDartVersion = Version(3, 6, 0);

Future<String> generateBuildScript() async {
  final builderFactories = await loadBuilderFactories();
  final library = Library(
    (b) => b.body.addAll([
      declareFinal('_builderFactories')
          .assign(
            refer(
              'BuilderFactories',
              'package:build_runner/src/build_plan/builder_factories.dart',
            ).call([], {
              'builderFactories': literalMap(builderFactories.builderFactories),
              'postProcessBuilderFactories': literalMap(
                builderFactories.postProcessBuilderFactories,
              ),
            }),
          )
          .statement,
      _main(),
    ]),
  );
  final emitter = DartEmitter(
    allocator: Allocator.simplePrefixing(),
    useNullSafetySyntax: true,
  );
  try {
    // The `build_runner` version number is included to force a rebuild if the
    // script was generated by a different version. It only needs updating if
    // the host<->isolate relationship changed in a breaking way, for example
    // if command line args or messages passed via sendports have changed
    // in a breaking way.
    return DartFormatter(languageVersion: _lastShortFormatDartVersion).format(
      '''
// @dart=${_lastShortFormatDartVersion.major}.${_lastShortFormatDartVersion.minor}
// ignore_for_file: directives_ordering
// build_runner >=2.4.16
${library.accept(emitter)}
''',
    );
  } on FormatterException {
    buildLog.error(
      'Generated build script could not be parsed. '
      'Check builder definitions.',
    );
    throw const CannotBuildException();
  }
}

Future<BuilderFactoriesExpressions> loadBuilderFactories() async {
  final packageGraph = await PackageGraph.forThisPackage();
  final orderedPackages = stronglyConnectedComponents<PackageNode>(
    [packageGraph.root],
    (node) => node.dependencies,
    equals: (a, b) => a.name == b.name,
    hashCode: (n) => n.name.hashCode,
  ).expand((c) => c);
  final readerWriter = ReaderWriter(packageGraph);
  final overrides = await findBuildConfigOverrides(
    packageGraph: packageGraph,
    readerWriter: readerWriter,
    configKey: null,
  );
  Future<BuildConfig> packageBuildConfig(PackageNode package) async {
    if (overrides.containsKey(package.name)) {
      return overrides[package.name]!;
    }
    try {
      return await BuildConfig.fromBuildConfigDir(
        package.name,
        package.dependencies.map((n) => n.name),
        package.path,
      );
    } on ArgumentError // ignore: avoid_catching_errors
    catch (_) {
      // During the build an error will be logged.
      return BuildConfig.useDefault(
        package.name,
        package.dependencies.map((n) => n.name),
      );
    }
  }

  bool isPackageImportOrForRoot(dynamic definition) {
    // ignore: avoid_dynamic_calls
    final import = definition.import as String;
    // ignore: avoid_dynamic_calls
    final package = definition.package as String;
    return import.startsWith('package:') || package == packageGraph.root.name;
  }

  final buildConfigs = await Future.wait(
    orderedPackages.map(packageBuildConfig),
  );
  final builderDefinitions =
      buildConfigs
          .expand((c) => c.builderDefinitions.values)
          .where(isPackageImportOrForRoot)
          .toList()
        ..sort((a, b) => a.key.compareTo(b.key));
  final postProcessBuilderDefinitions =
      buildConfigs
          .expand((c) => c.postProcessBuilderDefinitions.values)
          .where(isPackageImportOrForRoot)
          .toList()
        ..sort((a, b) => a.key.compareTo(b.key));

  return BuilderFactoriesExpressions(
    builderFactories: {
      for (final builder in builderDefinitions)
        builder.key: _builderFactories(builder),
    },
    postProcessBuilderFactories: {
      for (final postProcessBuilder in postProcessBuilderDefinitions)
        postProcessBuilder.key: _postProcessBuilderFactory(postProcessBuilder),
    },
  );
}

class BuilderFactoriesExpressions {
  final Map<String, Expression> builderFactories;
  final Map<String, Expression> postProcessBuilderFactories;

  BuilderFactoriesExpressions({
    required this.builderFactories,
    required this.postProcessBuilderFactories,
  });
}

/// A method forwarding to `run`.
Method _main() => Method((b) {
  b.name = 'main';
  b.returns = refer('void');
  b.modifier = MethodModifier.async;
  b.requiredParameters.add(
    Parameter((b) {
      b.name = 'args';
      b.type = TypeReference((b) {
        b.symbol = 'List';
        b.types.add(refer('String'));
      });
    }),
  );
  b.body = Block.of([
    refer('exitCode', 'dart:io')
        .assign(
          refer(
            'ChildProcess.run',
            'package:build_runner/src/bootstrap/processes.dart',
          ).call([refer('args'), refer('_builderFactories')]).awaited,
        )
        .nullChecked
        .statement,
  ]);
});

/// The `List` of `BuilderFactory` declared in [definition].
Expression _builderFactories(BuilderDefinition definition) {
  final import = _buildScriptImport(definition.import);
  return literalList([
    for (final f in definition.builderFactories) refer(f, import),
  ]);
}

/// The `PostProcessBuilderFactory` declared in [definition].
Expression _postProcessBuilderFactory(PostProcessBuilderDefinition definition) {
  final import = _buildScriptImport(definition.import);
  return refer(definition.builderFactory, import);
}

/// Returns the actual import to put in the generated script based on an import
/// found in the build.yaml.
String _buildScriptImport(String import) {
  if (import.startsWith('package:') ||
      import.startsWith('../') ||
      import.startsWith('/')) {
    return import;
  } else {
    return p.url.relative(import, from: p.url.dirname(entrypointScriptPath));
  }
}
